package org.termine.logic;

import java.io.InputStream;
import java.io.Serializable;
import java.sql.Connection;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.LinkedList;
import java.util.List;

import javax.faces.bean.ApplicationScoped;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.ManagedProperty;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.termine.dao.AppointmentDAO;
import org.termine.dao.AvailabilityDAO;
import org.termine.dao.LocationDAO;
import org.termine.entity.Appointment;
import org.termine.entity.Availability;
import org.termine.entity.ClientProviderScheduleEvent;
import org.termine.entity.Location;
import org.termine.entity.ProviderAvailability;
import org.termine.entity.ServiceSelectItem;
import org.termine.util.GeoCoder;

@ManagedBean
@ApplicationScoped
public class AppointmentManager extends BaseManager implements Serializable {

	public static final int MIN_PER_KILOMETER = 5;

	private static final long serialVersionUID = -2793576255997969542L;
	private static final Logger logger = LoggerFactory.getLogger(AppointmentManager.class);

	@ManagedProperty(value = "#{locationDAO}")
	private LocationDAO locationDAO;
	@ManagedProperty(value = "#{appointmentDAO}")
	private AppointmentDAO appointmentDAO;
	@ManagedProperty(value = "#{availabilityDAO}")
	private AvailabilityDAO availabilityDAO;
	@ManagedProperty(value = "#{availabilityManager}")
	private AvailabilityManager availabilityManager;

	public String addAppointment(Location location, ServiceSelectItem service, Date start,
			Integer clientId) {
		Calendar calendar = new GregorianCalendar();
		calendar.setTime(start);
		calendar.add(Calendar.MINUTE, service.getBookDeadlineInMin());
		if (calendar.getTime().before(new Date())) {
			return "NOT_WITHIN_BOOKING_DEADLINE";
		} else {
			Connection connection = null;
			try {
				connection = this.connector.getConnection();
				Calendar end = new GregorianCalendar();
				end.setTime(start);
				end.add(Calendar.MINUTE, service.getDurationInMin());
				Availability availability = this.getAvailabilityDAO().getAvailabilityForProvider(
						connection, service.getProviderId(), start, end.getTime(),
						Availability.AVAILABILITY);
				if (availability == null) {
					return "NO_AVAILABILITY";
				}

				Availability unavailability = this.getAvailabilityDAO().getAvailabilityForProvider(
						connection, service.getProviderId(), start, end.getTime(),
						Availability.UNAVAILABILITY);
				if (unavailability != null) {
					return "UNAVAILABLE";
				}

				if (getAppointmentDAO().overlapsWithExistingAppointment(connection, start,
						end.getTime(), service.getDurationInMin(), service.getProviderId())) {
					return "OVERLAP_WITH_APPOINTMENT";
				}
				
				GeoCoder.setPosition(location);

				ClientProviderScheduleEvent beforeAppointment = this
						.getAppointmentDAO()
						.getBeforeAppointmentForProvider(connection, service.getProviderId(), start);
				if (beforeAppointment != null) {
					double beforeDistance = this.getLocationDAO().getDistanceToLocation(connection,
							beforeAppointment.getLocationId(), location.getLongitude(),
							location.getLatitude());
					int beforeTravelTime = ((int) Math.round(beforeDistance)) * MIN_PER_KILOMETER;
					Calendar beforeEnd = new GregorianCalendar();
					beforeEnd.setTime(beforeAppointment.getEndDate());
					beforeEnd.add(Calendar.MINUTE, beforeTravelTime);
					if (!beforeEnd.getTime().before(start)) {
						return "BEFORE_APPOINTMENT_TRAVEL_OVERLAP";
					}
				} else {
					double beforeDistance = this.getLocationDAO().getDistanceToLocation(
							connection,
							this.getLocationDAO().getLocationForProvider(connection,
									service.getProviderId()), location.getLongitude(),
							location.getLatitude());
					int beforeTravelTime = ((int) Math.round(beforeDistance)) * MIN_PER_KILOMETER;
					Calendar beforeEnd = new GregorianCalendar();
					beforeEnd.setTime(availability.getStart());
					beforeEnd.add(Calendar.MINUTE, beforeTravelTime);
					if (!beforeEnd.getTime().before(start)) {
						return "BEFORE_APPOINTMENT_TRAVEL_OVERLAP";
					}
				}

				ClientProviderScheduleEvent afterAppointment = this.getAppointmentDAO()
						.getAfterAppointmentForProvider(connection, service.getProviderId(),
								end.getTime());
				if (afterAppointment != null) {
					double afterDistance = this.getLocationDAO().getDistanceToLocation(connection,
							afterAppointment.getLocationId(), location.getLongitude(),
							location.getLatitude());
					int afterTravelTime = ((int) Math.round(afterDistance)) * MIN_PER_KILOMETER;
					Calendar afterEnd = new GregorianCalendar();
					afterEnd.setTime(start);
					afterEnd.add(Calendar.MINUTE, afterTravelTime);
					if (!afterEnd.getTime().before(afterAppointment.getAppointmentStart())) {
						return "AFTER_APPOINTMENT_TRAVEL_OVERLAP";
					}
				}

				this.getLocationDAO().addLocation(connection, location);
				Appointment appointment = new Appointment();
				appointment.setClient_id(clientId);
				appointment.setService_id(service.getServiceId());
				appointment.setLocation_id(this.getLocationDAO()
						.getLocationId(connection, location));
				appointment.setStart(start);
				this.getAppointmentDAO().addAppointment(connection, service, appointment);
				this.connector.commit(connection);
				return "SUCCESS";
			} catch (Exception e) {
				this.connector.rollback(connection);
				logger.error("ERROR: ", e);
				return "ERROR";
			}
		}
	}

	public boolean cancelAppointment(ClientProviderScheduleEvent event, String whoAmI) {
		boolean result = false;
		Connection connection = null;
		try {
			connection = this.connector.getConnection();
			if (whoAmI.equals("PROVIDER")) { // a provider can always cancel
				this.deleteAppointment(event, connection);
				result = true;
			} else { // a client cannot cancel after the deadline
				Calendar now = new GregorianCalendar();
				Calendar deadline = new GregorianCalendar();
				deadline.add(Calendar.MINUTE, (-1) * event.getServiceCancelDeadlineInMin());
				if (deadline.after(now)) {
					result = false;
				} else {
					this.deleteAppointment(event, connection);
					result = false;
				}
			}
			this.connector.commit(connection);
			return result;
		} catch (Exception e) {
			this.connector.rollback(connection);
			logger.error("ERROR: ", e);
			return false;
		}
	}

	private void deleteAppointment(ClientProviderScheduleEvent event, Connection connection)
			throws Exception {
		this.getAppointmentDAO().deleteAppointment(connection, event.getAppointmentId());
	}

	public List<ClientProviderScheduleEvent> getAllAppointmentsForProvider(Integer providerId) {
		Connection connection = null;
		try {
			connection = this.connector.getConnection();
			return this.getAppointmentDAO().getAllAppointmentsForProvider(connection, providerId);
		} catch (Exception e) {
			logger.error("ERROR: ", e);
			return null;
		} finally {
			this.connector.closeConnection(connection);
		}
	}

	public InputStream getAllAppointmentsForProvider(Date from, Date to, Integer providerId) {
		if (from != null && to != null) {
			Connection connection = null;
			try {
				connection = this.connector.getConnection();
				return ClientProviderScheduleEvent.toXCalXml(this.getAppointmentDAO()
						.getAllAppointmentsForProvider(connection, providerId, from, to));
			} catch (Exception e) {
				logger.error("ERROR: ", e);
				return null;
			} finally {
				this.connector.closeConnection(connection);
			}
		} else {
			return null;
		}
	}

	public List<ClientProviderScheduleEvent> getAllAppointmentsForClient(Integer clientId) {
		Connection connection = null;
		try {
			connection = this.connector.getConnection();
			return this.getAppointmentDAO().getAllAppointmentsForClient(connection, clientId);
		} catch (Exception e) {
			logger.error("ERROR: ", e);
			return null;
		} finally {
			this.connector.closeConnection(connection);
		}
	}

	public List<ProviderAvailability> getAllProviderAvailabilitiesForProvider(Integer providerId) {
		Connection connection = null;
		try {
			connection = this.connector.getConnection();
			List<Availability> availabilities = this.getAvailabilityManager()
					.getActualAvailabilitiesForProvider(providerId);
			List<ProviderAvailability> result = new LinkedList<ProviderAvailability>();
			for (int i = 0; i < availabilities.size(); i++) {
				ProviderAvailability availability = new ProviderAvailability();
				availability.setStart(availabilities.get(i).getStart());
				availability.setEnd(availabilities.get(i).getEnd());
				availability.setAppointments(this.getAppointmentDAO()
						.getAllProviderAppointmentsForProvider(connection, providerId,
								availabilities.get(i).getStart(), availabilities.get(i).getEnd()));
				result.add(availability);
			}
			return result;
		} catch (Exception e) {
			logger.error("ERROR: ", e);
			return null;
		} finally {
			this.connector.closeConnection(connection);
		}
	}

	/*
	 * Getters & Setters
	 */
	public LocationDAO getLocationDAO() {
		return this.locationDAO;
	}

	public void setLocationDAO(LocationDAO locationDAO) {
		this.locationDAO = locationDAO;
	}

	public AvailabilityManager getAvailabilityManager() {
		return this.availabilityManager;
	}

	public void setAvailabilityManager(AvailabilityManager availabilityManager) {
		this.availabilityManager = availabilityManager;
	}

	public AppointmentDAO getAppointmentDAO() {
		return this.appointmentDAO;
	}

	public void setAppointmentDAO(AppointmentDAO appointmentDAO) {
		this.appointmentDAO = appointmentDAO;
	}

	public AvailabilityDAO getAvailabilityDAO() {
		return this.availabilityDAO;
	}

	public void setAvailabilityDAO(AvailabilityDAO availabilityDAO) {
		this.availabilityDAO = availabilityDAO;
	}

}
